/*
  SGFFilter.cs -- a filter for Beagle[1] that parses SGF[2] files.

  [1] - http://beagle-project.org/
  [2] - http://www.red-bean.com/sgf/

  Unlike the rest of Gimme Go (which is GPL2), this file is released
  under the X11 / MIT license, which the Beagle project uses, so that
  they may include it as part of Beagle if they wish.

  Ken Harris <kengruven@gmail.com>
  2006.9.29
*/

using System.IO;
using Beagle.Daemon;

using System.Text.RegularExpressions;
using System.Collections;
using System.Text;
 
/*
  WAYS THIS FILTER COULD BE BETTER:

  - it's my first C# program, and also my first Beagle filter, so it's
  probably really lousy code

  - it doesn't deal with SGF CA[] encodings (though to be fair
  encodings are pretty broken in SGF); no guarantees, if your file
  isn't ASCII / Latin-1

  - I don't know what Beagle properties are allowed / desired; for
  now, I fill dc:title, fixme:height and fixme:width.  (Can I just
  make up my own?)

  - I don't do much checking of field types at all; if it's not
  obviously a point on the board, I save it.

  - \-escaped characters aren't unescaped.  Whitespace isn't collapsed
  for Text/SimpleText fields (though maybe beagle does some of that
  for me?)

  - Beagle has something called "child indexables" that sound
  appropriate for dealing with multiple games stuffed into one SGF
  file, but I don't know how to use them yet

  - Compiling the regexes in DoOpen() doesn't seem to help; but would
  it if they were compiled outside of it?  (Are Filters reused?)
*/

namespace Beagle.Filters {
    public class SGFFilter : Beagle.Daemon.Filter {

	ArrayList comments; // text i load in DoOpen, and then regurgitate in DoPull
	Hashtable properties; // properties, also loaded in DoOpen and used in DoPullProperties

 	public SGFFilter ()
 	{
 	    AddSupportedFlavor (FilterFlavor.NewFromMimeType ("application/x-go-sgf"));
	    AddSupportedFlavor (FilterFlavor.NewFromExtension (".sgf"));

	    SetVersion(1); // ??

	    comments = new ArrayList();
	    properties = new Hashtable();
 	}
 
 	override protected void DoOpen (FileInfo file)
 	{
	    // whip together a pair of regular expressions for parsing
	    // (SGF, if you ignore branching, is a regular language)
	    string ignore_str = "(?:[(); \\n\\r]*)";
	    string key_str = "([A-Z]{1,2})";
	    
	    string value_nonquoted_str = "[^\\]]";
	    string value_quoted_str = "\\\\.";
	    string value_str = "(\\[(?:" + value_quoted_str + "|" + value_nonquoted_str + ")*\\])";

	    // compiling these doesn't seem to help, but we can always hope
	    Regex ignore_and_key_re = new Regex(ignore_str + key_str, RegexOptions.Compiled);
	    Regex ignore_and_value_re = new Regex(ignore_str + value_str, RegexOptions.Compiled);

	    // read entire file into a string, so I can use my REs to parse it
	    Encoding encoding = Encoding.GetEncoding("latin1"); // (C#'s name for Latin-1)
	    StreamReader reader = new StreamReader (file.FullName, encoding);
	    string text = reader.ReadToEnd();
	    reader.Close();

	    int i=0; // position to start reading (index into |text|)
	    string key=null; // the last key read -- any values read are values for this key

	    while (true) {
		Match key_match = ignore_and_key_re.Match(text, i);
		if (key_match.Success && key_match.Groups[0].Captures[0].Index==i) {
		    /*
		      In Python, you can re.search() or re.match(),
		      with the latter requiring a match to begin right
		      here; in C#, it seems you can't do that:
		      Match(text,i) matches starting here but doesn't
		      require a match to start here, and prepending
		      "^" to the RE means "start of string", not
		      "start at i".  (Saying
		      some_re.Match(text.Substring(i)) probably works
		      great, but is horribly inefficient.)

		      As a workaround, I do Match(text,i), and then
		      count it as a match only if .Success is set,
		      *and* the zeroth capture starts at this
		      position.  (Is this a common enough C# idiom
		      that I can omit this explanation?)
		     */

		    // set key, increment i
		    key = key_match.Groups[1].Value;
		    i += key_match.Groups[0].Captures[0].Length;

		    continue;
		}

		Match value_match = ignore_and_value_re.Match(text, i);
		if (value_match.Success && value_match.Groups[0].Captures[0].Index==i) {
		    // set |key|, increment |i| (from span)
		    string value = value_match.Groups[1].Value;
		    i += value_match.Groups[0].Captures[0].Length;

		    // crop off []'s
		    value = value.Substring(1, value.Length-2);

		    // (TODO: improve this part: use real types: Text/SimpleText/etc.)

		    // save value!  but only if it's human-readable text
		    if (key!="B" && key!="W" && key!="TB" && key!="TW" && 
			key!="SQ" && key!="TR" && key!="CR" && key!="MA") {
			comments.Add(value);
		    }

		    // if it's something i care about, save it in the proper place
		    if (key=="GN" && !properties.ContainsKey("dc:title")) {
			// kogo's has 2 GN's, and the first one is the one i want.
			// i don't know if that's valid, or if i can report both to beagle,
			// or what ... for now, i'll take the first one, and call it quits.
			properties["dc:title"] = value;
		    }
		    if (key=="SZ") {
			// FUTURE: allow for non-square boards
			properties["fixme:height"] = value;
			properties["fixme:width"] = value;
		    }

		    continue;
		}

		// if we can't find a key or a value, we're done
		break;
	    }

 	}
 
	// this method adds properties, from dublin core (like "author")
	// DT=date, SY=???, GN=name, PB/PW=player names (authors?)
 	override protected void DoPullProperties ()
 	{
 	    // AddProperty (Beagle.Property.New ("fixme:comment", "Hello world"));

	    IDictionaryEnumerator en = properties.GetEnumerator();
	    while (en.MoveNext()) {
		AddProperty(Beagle.Property.New(en.Key.ToString(), en.Value.ToString()));
	    }

	    // it looks like they're all of the form "fixme:%s" (really!)
	    // or "dc:%s" for dublin core.  "dc:title".  (for GN?)
 	}
 	
	// then, this method adds raw text: C=comment go here; also, SZ=size?
 	override protected void DoPull ()
 	{
	    foreach (string line in comments) {
		AppendText(line);
		AppendStructuralBreak();
	    }
	    Finished();
 	}
    }
}

/*
  TO TEST:

  mcs -t:library -r:/usr/lib/beagle/BeagleDaemonPlugins.dll -r:/usr/lib/beagle/Beagle.dll -out:SGFFilter.dll Source/SGFFilter.cs

  sudo cp SGFFilter.dll /usr/lib/beagle/Filters/

  beagle-extract-content Symbols.sgf
*/
